const {Tapable, SyncHook} = require("tapable")
const Parser = require('./Parser')
const path = require("path");
const NormalModuleFactory = require("./NormalModuleFactory");
const async = require('neo-async')
const Chunk = require("./Chunk");
const ejs = require('ejs')

const normalModuleFactory = new NormalModuleFactory()
const parser = new Parser()

class Compilation extends Tapable {
    constructor(compiler) {
        super()
        this.compiler = compiler
        this.context = compiler.context
        this.options = compiler.options
        // compilation具备文件读写能力
        this.inputFileSystem = compiler.inputFileSystem
        this.outputFileSystem = compiler.outputFileSystem
        this.entries = [] // 存放所有入口模块
        this.modules = [] // 存放所有模块
        this.chunks = [] // 存放当前次打包产出的chunk
        this.assets = []
        this.files = []
        this.hooks = {
            succeedModule: new SyncHook(['module']),
            seal: new SyncHook(),
            beforeChunks: new SyncHook(),
            afterChunks: new SyncHook(),
        }
    }

    /**
     * 完成模块编译
     * @param context 当前项目的根
     * @param entry 当前入口的相对路径
     * @param name chunkName: main
     * @param callback 回调
     */
    addEntry(context, entry, name, callback) {
        this._addModuleChain(context, entry, name, (err, module) => {
            callback(err, module)
        })
    }

    _addModuleChain(context, entry, name, callback) {
        this.createModule({
            parser,
            name: name,
            context: context,
            // 原始请求路径
            rawRequest: entry,
            resource: path.posix.join(context, entry),
            moduleId: './' + path.posix.relative(context, path.posix.join(context, entry))
        }, (entryModule) => {
            this.entries.push(entryModule)
        }, callback)
    }

    /**
     * 创建模块的方法
     * @param data 创建模块需要的属性值
     * @param doAndEntry 可选参数, 在加载入口模块的时候, 将入口模块的id 写入 this.entries
     * @param callback
     */
    createModule(data, doAndEntry, callback) {
        let module = normalModuleFactory.create(data)

        const afterBuild = (err, module) => {
            // 需要判断当前module加载完成之后是否需要处理依赖加载
            if (module.dependencies.length > 0) {
                // 当前逻辑表示module有需要依赖加载的模块, 单独定义一个方法实现
                this.processDependencies(module, (err) => {
                    callback(err, module)
                })
            } else {
                callback(err, module)
            }
        }

        this.buildModule(module, afterBuild)

        // 完成本次build后, 保存模块
        // this.entries.push(module)
        this.modules.push(module)
        doAndEntry && doAndEntry(module)
    }

    /**
     * 完成具体的 build 行为
     * @param module 当前需要被编译的模块
     * @param callback
     */
    buildModule(module, callback) {
        // module -> entryModule
        module.build(this, (err) => {
            // 走到这里表示当前代码编译完成
            this.hooks.succeedModule.call(module)
            callback(err, module)
        })
    }

    processDependencies(module, callback) {
        // 实现被依赖模块的加载
        // 创建对应模块,然后想办法拿出其中的内容
        // 在所有被依赖模块加载完后再 callback
        let dependencies = module.dependencies

        async.forEach(dependencies, (dependency, done) => {
            // 调用done表示执行完毕
            this.createModule({
                parser,
                name: dependency.name,
                context: dependency.context,
                rawRequest: dependency.rawRequest,
                moduleId: dependency.moduleId,
                resource: dependency.resource
            }, null, done)
        }, callback)
    }

    // 封装chunk
    seal(callback) {
        this.hooks.seal.call()
        this.hooks.beforeChunks.call()

        // 所有入口模块都存放在了 compilation 对象的 entires 数组里
        // 封装 chunk 就是根据某个入口，将它所有依赖和源代码放在一起，之后做合并
        for (const entryModule of this.entries) {
            // 核心：创建模块加载已有模块内容，同时记录模块信息
            const chunk = new Chunk(entryModule)

            this.chunks.push(chunk)

            // 给chunk赋值
            chunk.modules = this.modules.filter(module => module.name === chunk.name)
        }

        // chunk流程处理完就进入chunk代码处理环节(模板文件+模块中的源代码 -> chunk.js)
        this.hooks.afterChunks.call(this.chunks)

        // 生成代码内容
        this.createChunkAssets()

        callback()
    }

    createChunkAssets() {
        for (let i = 0; i < this.chunks.length; i++) {
            const chunk = this.chunks[i]
            const fileName = chunk.name + '.js'
            chunk.files.push(fileName)

            // 生成具体的chunk内容
            // 获取模板文件的路径
            let tempPath = path.posix.join(__dirname, 'temp/main.ejs')
            // 读取模块文件中的内容
            let tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf-8')
            // 获取渲染函数
            let tempRender = ejs.compile(tempCode)
            // 渲染数据
            let source = tempRender({
                entryModuleId: chunk.entryModule.moduleId,
                modules: chunk.modules
            })
            // 输出文件
            this.emitAssets(fileName, source)
        }
    }

    emitAssets(fileName, source) {
        this.assets[fileName] = source
        this.files.push(fileName)
    }
}

module.exports = Compilation